package edu.gatech.spacetraderteam3.view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

import edu.gatech.spacetraderteam3.debug.Debug;

/**
 * Applies filters to images shown in all map views
 * 
 * @author Alexander Ikonomidis
 * @version 1.0
 */
public class ImageFilters {

    // ROW, COLUMN
    /**
     * Fast Image blur effect
     * 
     * @param src
     *            Image to apply the effect to
     * @param amount
     *            Amount of blur to apply
     * @return The blurred image
     */
    public static BufferedImage fastBlur(BufferedImage src, double amount) {
        long millis = System.currentTimeMillis();
        int wid = src.getWidth();
        int hei = src.getHeight();
        BufferedImage out = new BufferedImage(wid, hei, BufferedImage.TYPE_INT_ARGB);
        BufferedImage derp = new BufferedImage((int) (src.getWidth() * amount),
                (int) (src.getHeight() * amount), BufferedImage.TYPE_INT_ARGB);

        Graphics2D g2 = (Graphics2D) derp.getGraphics();
        g2.setRenderingHint(RenderingHints.KEY_RENDERING,
                            RenderingHints.VALUE_RENDER_QUALITY);
        g2.drawImage(src, 0, 0, derp.getWidth(), derp.getHeight(), null);

        g2 = (Graphics2D) out.getGraphics();
        g2.setRenderingHint(RenderingHints.KEY_RENDERING,
                            RenderingHints.VALUE_RENDER_QUALITY);
        g2.drawImage(derp, 0, 0, wid, hei, null);

        Debug.write("Fast blur completed in: " + (System.currentTimeMillis() - millis), 2);
        return out;
    }

    /**
     * Box blur effect
     * 
     * @param src
     *            Image to blur
     * @return Blurred image
     */
    public static BufferedImage boxBlur(BufferedImage src) {
        long millis = System.currentTimeMillis(), temp = millis;
        int wid = src.getWidth();
        int hei = src.getHeight();
        BufferedImage out = new BufferedImage(wid, hei, BufferedImage.TYPE_INT_ARGB);
        int[][][] srcChannels = new int[4][hei][wid]; // RGBA
        int[] pixelBuffer = new int[4 * hei * wid];
        int cI;
        for (int j = 0; j <= hei; j++) {
            for (int i = 0; i <= wid; i++) {
                if (j < hei && i < wid) {
                    cI = src.getRGB(i, j);
                    srcChannels[0][j][i] = cI >> 16 & 0xFF;
                    srcChannels[1][j][i] = cI >> 8 & 0xFF;
                    srcChannels[2][j][i] = cI & 0xFF;
                    pixelBuffer[(j * wid + i) * 4 + 3] = cI >> 24 & 0xFF;
                }
                if (i > 0 && j > 0) {
                    for (int c = 0; c < 3; c++) {// no alpha :X
                        //@formatter:off
                        if ((j-1) == 0) {
                            if ((i-1) == 0) {// top left
                                pixelBuffer[((j-1) * wid + (i-1)) * 4 + c] = avg(srcChannels[c][(j-1)][(i-1)],   srcChannels[c][(j-1)][(i-1)+1],
                                                                         srcChannels[c][(j-1)+1][(i-1)], srcChannels[c][(j-1)+1][(i-1)+1]);
                            } else if ((i-1) == wid - 1) { // bottom left
                                pixelBuffer[((j-1) * wid + (i-1)) * 4 + c] = avg(srcChannels[c][(j-1)][(i-1)-1],   srcChannels[c][(j-1)][(i-1)],
                                                                         srcChannels[c][(j-1)+1][(i-1)-1], srcChannels[c][(j-1)+1][(i-1)]);
                            } else {// top line
                                pixelBuffer[((j-1) * wid + (i-1)) * 4 + c] = avg(srcChannels[c][(j-1)][(i-1)-1],   srcChannels[c][(j-1)][(i-1)],   srcChannels[c][(j-1)][(i-1)+1],
                                                                         srcChannels[c][(j-1)+1][(i-1)-1], srcChannels[c][(j-1)+1][(i-1)], srcChannels[c][(j-1)+1][(i-1)+1]);
                            }
                        } else if ((j-1) == hei - 1) {
                            if ((i-1) == 0) {
                                pixelBuffer[((j-1) * wid + (i-1)) * 4 + c] = avg(srcChannels[c][(j-1)-1][(i-1)], srcChannels[c][(j-1)-1][(i-1)+1],
                                                                         srcChannels[c][(j-1)][(i-1)],   srcChannels[c][(j-1)][(i-1)+1]);
                            } else if ((i-1) == wid - 1) {// bottom right
                                pixelBuffer[((j-1) * wid + (i-1)) * 4 + c] = avg(srcChannels[c][(j-1)-1][(i-1)-1], srcChannels[c][(j-1)-1][(i-1)],
                                                                         srcChannels[c][(j-1)][(i-1)-1],   srcChannels[c][(j-1)][(i-1)]);
                            } else {// bottom line
                                pixelBuffer[((j-1) * wid + (i-1)) * 4 + c] = avg(srcChannels[c][(j-1)-1][(i-1)-1], srcChannels[c][(j-1)-1][(i-1)], srcChannels[c][(j-1)-1][(i-1)+1],
                                                                         srcChannels[c][(j-1)][(i-1)-1],   srcChannels[c][(j-1)][(i-1)],   srcChannels[c][(j-1)][(i-1)+1]);
                            }
                        } else if ((i-1) == 0) {// left line
                            pixelBuffer[((j-1) * wid + (i-1)) * 4 + c] = avg(srcChannels[c][(j-1)-1][(i-1)], srcChannels[c][(j-1)-1][(i-1)+1],
                                                                     srcChannels[c][(j-1)][(i-1)],   srcChannels[c][(j-1)][(i-1)+1],
                                                                     srcChannels[c][(j-1)+1][(i-1)], srcChannels[c][(j-1)+1][(i-1)+1]);
                        } else if ((i-1) == wid - 1) {// right line
                            pixelBuffer[((j-1) * wid + (i-1)) * 4 + c] = avg(srcChannels[c][(j-1)-1][(i-1)-1], srcChannels[c][(j-1)-1][(i-1)],
                                                                     srcChannels[c][(j-1)][(i-1)-1],   srcChannels[c][(j-1)][(i-1)],
                                                                     srcChannels[c][(j-1)+1][(i-1)-1], srcChannels[c][(j-1)+1][(i-1)]);
                        } else{
                            pixelBuffer[((j-1) * wid + (i-1)) * 4 + c] = avg(srcChannels[c][(j-1)-1][(i-1)-1], srcChannels[c][(j-1)-1][(i-1)], srcChannels[c][(j-1)-1][(i-1)+1],
                                                                     srcChannels[c][(j-1)][(i-1)-1],   srcChannels[c][(j-1)][(i-1)],   srcChannels[c][(j-1)][(i-1)+1],
                                                                     srcChannels[c][(j-1)+1][(i-1)-1], srcChannels[c][(j-1)+1][(i-1)], srcChannels[c][(j-1)+1][(i-1)+1]);
                        }
                        //@formatter:on
                    }
                }
            }
        }
        Debug.write("Arrays filled and pixels averaged in: "
                            + (-temp + (temp = System.currentTimeMillis()))
                            + " milliseconds.", 2);
        out.getRaster().setPixels(0, 0, wid, hei, pixelBuffer);
        Debug.write("Image constructed in: "
                            + (-temp + (temp = System.currentTimeMillis()))
                            + " milliseconds.", 2);
        return out;
    }

    /**
     * Cool Wobble effect used in suns
     * 
     * @param src
     *            Image to wobble
     * @param time
     *            Time used to calculate wobbe
     * @param amountX
     *            Amount of wobble in the X
     * @param amountY
     *            Amount of wobble in the Y
     * @return Dat wobbled src
     */
    public static BufferedImage wobble(BufferedImage src, double time, double amountX,
            double amountY) {
        int wid = src.getWidth();
        int hei = src.getHeight();
        BufferedImage out = new BufferedImage(wid, hei, BufferedImage.TYPE_INT_ARGB);
        int[] srcBuffer = src.getRaster().getPixels(0, 0, wid, hei,
                                                    new int[wid * hei * 4]);
        int[] pixelBuffer = new int[wid * hei * 4];
        int i2, j2, element, widL = wid - 1, heiL = hei - 1;
        for (int j = 0; j < hei; j++) {
            for (int i = 0; i < wid; i++) {
                i2 = Math
                        .max(0, Math.min(widL, i + (int) (amountX * Math.cos(time + j))));
                j2 = Math
                        .max(0, Math.min(heiL, j + (int) (amountY * Math.sin(time + i))));
                element = ((j2) * wid + (i2)) * 4;
                // if (element < wid * hei * 4)
                for (int c = 0; c < 4; c++) {// no alpha :X
                    pixelBuffer[((j) * wid + (i)) * 4 + c] = srcBuffer[element + c];
                }
            }
        }
        out.getRaster().setPixels(0, 0, wid, hei, pixelBuffer);
        return out;
    }

    /**
     * Rotates an image
     * 
     * @param src
     *            image to rotate
     * @param theta
     *            Angle to rotate
     * @return rotated image
     */
    public static BufferedImage rotate(BufferedImage src, double theta) { // TODO: make
                                                                          // this... not
                                                                          // crop? or make
                                                                          // another
                                                                          // method that
                                                                          // doesn't?
        BufferedImage out = new BufferedImage(src.getWidth(), src.getHeight(),
                BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = (Graphics2D) out.getGraphics();
        g.translate(src.getWidth() / 2, src.getHeight() / 2);
        g.rotate(-theta);
        g.drawImage(src, -src.getWidth() / 2, -src.getHeight() / 2, null);
        g.rotate(theta);
        g.translate(-src.getWidth() / 2, -src.getHeight() / 2);
        return out;
    }

    /**
     * Makes a square image into a sphere
     * 
     * @param src
     *            Source image
     * @return Spherized image
     */
    public static BufferedImage spherize(BufferedImage src) {
        int wid = src.getWidth(), hei = src.getHeight(), bufferSize = wid * hei * 4;
        double skip = 0, aSq, pixel = 0, halfWidth = wid / 2.0, halfHeight = hei / 2.0;
        BufferedImage out = new BufferedImage(wid, hei, BufferedImage.TYPE_INT_ARGB);
        int[] srcBuffer = src.getRaster().getPixels(0, 0, wid, hei, new int[bufferSize]);
        int[] outBuffer = new int[bufferSize];
        boolean start = false;
        for (int i = 0; i < hei; i++, start = false, pixel = 0) {
            aSq = Math.pow(halfHeight - i, 2);
            for (int j = 0; j < wid && pixel < wid; j++) {
                if (!start && Math.sqrt(aSq + Math.pow(halfWidth - j, 2)) <= halfWidth) {
                    skip = wid / (wid - j * 2.0); // VERY IMPORTANT THIS IS ZERO,
                                                  // DANIEL-SAN
                    start = true;
                }
                if (start) {
                    outBuffer[(i * hei + (int) (j)) * 4] = srcBuffer[(i * hei + (int) pixel) * 4];
                    outBuffer[(i * hei + (int) (j)) * 4 + 1] = srcBuffer[(i * hei + (int) pixel) * 4 + 1];
                    outBuffer[(i * hei + (int) (j)) * 4 + 2] = srcBuffer[(i * hei + (int) pixel) * 4 + 2];
                    outBuffer[(i * hei + (int) (j)) * 4 + 3] = srcBuffer[(i * hei + (int) pixel) * 4 + 3];
                    pixel += skip;
                }
            }
        }
        out.getRaster().setPixels(0, 0, wid, hei, outBuffer);
        return out;
    }

    /**
     * Seamless crop utility
     * 
     * @param src
     *            Source image to crop
     * @param start
     *            Where to start cropping
     * @return Cropped images
     */
    public synchronized static BufferedImage seamlessSquareCrop(BufferedImage src,
            double start) { // input must be horizontally rectangular
        int wid = src.getWidth();
        int hei = src.getHeight();
        float[] cl = new float[4];
        BufferedImage out = new BufferedImage(hei, hei, BufferedImage.TYPE_INT_ARGB);
        Raster srcRaster = src.getRaster();
        WritableRaster outRaster = out.getRaster();
        if (src.getAlphaRaster() == null) {
            for (int row = 0; row < hei; row++) {
                for (int col = (int) start; col < hei + start; col++) {
                    cl = srcRaster.getPixel(col % wid, row, cl);// getRGB(col % wid, row);
                    cl[3] = 255;
                    outRaster.setPixel((int) (col - start), row, cl);
                }
            }

        } else {
            for (int row = 0; row < hei; row++) {
                for (int col = (int) start; col < hei + start; col++) {
                    outRaster.setPixel((int) (col - start), row,
                                       srcRaster.getPixel(col % wid, row, cl));
                }
            }
        }
        return out;
    }

    /**
     * Pulse effect
     * 
     * @param src
     *            Source image to pulse
     * @param amount
     *            Ammount to pulse
     * @return Pulsed image
     */
    public static BufferedImage pulse(BufferedImage src, double amount) { // 1.0 is
                                                                          // original size
        double rat = 1.0 * src.getWidth() / src.getHeight();
        int wid = (int) (src.getWidth() + Math.cos(amount) * rat * 30);
        int hei = (int) (src.getHeight() + Math.cos(amount) * 30);
        int x = src.getWidth() / 2 - wid / 2;
        int y = src.getHeight() / 2 - hei / 2;
        BufferedImage out = new BufferedImage(src.getWidth(), src.getHeight(),
                BufferedImage.TYPE_INT_ARGB);
        out.getGraphics().drawImage(src, x, y, wid, hei, null);
        return out;
    }

    /**
     * Resizes an image
     * 
     * @param src
     *            Source image
     * @param wid
     *            Width to resize to
     * @param hei
     *            Height to resize to
     * @return Resized image
     */
    public static BufferedImage resize(BufferedImage src, int wid, int hei) {
        BufferedImage out = new BufferedImage(wid, hei, BufferedImage.TYPE_INT_ARGB);
        Graphics g = out.getGraphics();
        g.drawImage(src, 0, 0, wid, hei, null);
        return out;
    }

    /**
     * Averages numbers
     * 
     * @param nums
     *            numbers to average
     * @return the average
     */
    private static int avg(int... nums) {
        return sum(nums) / nums.length;
    }

    /**
     * Sums numbers
     * 
     * @param nums
     *            Numbers to sum
     * @return The sum
     */
    private static int sum(int... nums) {
        int sum = 0;
        for (int d : nums) {
            sum += d;
        }
        return sum;
    }

    /**
     * Scale and time increment constants
     */
    private static double scale = 2, timeIncrement = 10;

    /**
     * Test image
     */
    private static BufferedImage b = getTestImage(200, 100); // a;

    /**
     * Constructs and gets test image
     * 
     * @param wid
     *            Width of the test image
     * @param hei
     *            Height of the test image
     * @return The test image
     */
    private static BufferedImage getTestImage(int wid, int hei) {
        BufferedImage b = new BufferedImage(wid, hei, BufferedImage.TYPE_INT_ARGB);
        Graphics g = b.getGraphics();
        g.setColor(Color.GREEN);
        g.fillRect(wid / 5, hei / 5, 3 * wid / 5, 3 * hei / 5);
        g.setColor(Color.BLUE);
        g.fillRect(0, 0, wid / 3, hei / 3);
        g.setColor(Color.RED);
        g.fillRect(0, 2 * hei / 3, wid / 3, hei / 3);
        g.setColor(Color.ORANGE);
        g.fillRect(2 * wid / 3, 0, wid / 3, hei / 3);
        g.setColor(Color.MAGENTA);
        g.fillRect(2 * wid / 3, 2 * hei / 3, wid / 3, hei / 3);
        g.setColor(Color.CYAN);
        g.fillRect(2 * wid / 5, 2 * hei / 5, wid / 5, hei / 5);
        return b;
    }

    /**
     * @return this is the image filter utility class.
     */
    public String toString() {
        return "Image Filter utility class.";
    }

    /**
     * Test for different image filters
     * 
     * @param args
     *            Arguments when running
     * @throws IOException
     *             We don't care about these so we throw them
     */
    @SuppressWarnings("serial")
    public static void main(String[] args) throws IOException {
        final JPanel p = new JPanel() {
            double time = 0;
            {
                if (timeIncrement > 0)
                    new Timer(100, new ActionListener() {
                        public void actionPerformed(ActionEvent arg0) {
                            time += timeIncrement;
                            repaint();
                        }
                    }).start();
            }

            /**
             * Paint's tests
             */
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                g.setColor(Color.BLACK);
                g.fillRect(0, 0, getWidth(), getHeight());
                BufferedImage buff = new BufferedImage(b.getWidth() * 2, b.getHeight(),
                        BufferedImage.TYPE_INT_ARGB);
                Graphics g2 = buff.getGraphics();
                g2.drawImage(b, 0, 0, this);
                BufferedImage c = b;
                // c = wobble(c, time, 8, 0);
                c = seamlessSquareCrop(c, (int) time);
                // c = pulse(c, time);
                // c = fastBlur(c, .5);
                // c = rotate(c, time / 10);
                c = spherize(c);
                g2.drawImage(c, b.getWidth() + 1, 0, this);
                g.drawImage(buff, 0, 0, (int) (b.getWidth() * scale * 2),
                            (int) (b.getHeight() * scale), null);
            }

        };
        p.setPreferredSize(new Dimension((int) (b.getWidth() * scale * 2), (int) (b
                .getHeight() * scale)));
        JFrame f = new JFrame("Filter demo");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(p);
        f.pack();
        f.setVisible(true);
    }
}